﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Collections.Generic;

namespace WMemoryProfiler
{
    /// <summary>
    /// Simple wrapper around cdb to execute debugger commands from within .NET to self debug a process
    /// </summary>
    public class CdbEng : IDisposable
    {
        static bool ForceDebuggerWindowVisible = Environment.GetEnvironmentVariable("FORCE_DEBUGGER_VISIBLE") != null;

        Process CdbServer;
        ProcessStartInfo StartPolicy = new ProcessStartInfo 
        {
            WindowStyle = ForceDebuggerWindowVisible ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
            UseShellExecute = true,
        };

        int Pid;
        string CdpPath;

        const string CommandToken = "$Command$";
        string CommandScript = CommandToken + Environment.NewLine +
                               @".remote_exit g;";
        string CommandFile;
        string LogFileName;

        /// <summary>
        /// Is false if we want to debug ourself and the process was already started under a debugger.
        /// In this case we play dead and do nothing.
        /// </summary>
        protected bool CanDebugProcess;

        /// <summary>
        /// True when debugger was already closed.
        /// </summary>
        public bool IsDisposed
        {
            get;
            private set;
        }

        /// <summary>
        /// True if we are debugging ourself
        /// </summary>
        public bool IsSelfDebugging
        {
            get;
            private set;
        }

        /// <summary>
        /// Attach debugger to current process
        /// </summary>
        /// <param name="debuggerWindowStyle">Optional debugger window style. Default is hidden.</param>
        public CdbEng(ProcessWindowStyle debuggerWindowStyle = ProcessWindowStyle.Hidden)
            : this(ProcessInfos.SelfPid, debuggerWindowStyle, DebuggerEvents.None)
        { }

        /// <summary>
        /// Attach debugger to specified process
        /// </summary>
        /// <param name="pid">process id</param>
        /// <param name="debuggerWindowStyle">Optional debugger window style. Default is hidden.</param>
        /// <param name="toIgnore">When the debugger is started several sxn commands are issued to prevent the debugger to break and cause therfore hangs during self debugging from which we cannot recover.</param>
        public CdbEng(int pid, ProcessWindowStyle debuggerWindowStyle = ProcessWindowStyle.Hidden, DebuggerEvents toIgnore = DebuggerEvents.None)
        {
            Pid = pid;
            CdpPath = ProcessInfos.GetMatchingCdb(pid);
            IsSelfDebugging = ProcessInfos.SelfPid == pid;

            // Play dead man if we are started already under the debugger
            CanDebugProcess = (Debugger.IsAttached && IsSelfDebugging) ? false : true;
            Debug.Print("CanDebugProcess: {0}, IsSelfDebugging: {1}, CdbPath: {2}", CanDebugProcess, IsSelfDebugging, CdpPath);

            if(!CanDebugProcess)
            {
                Trace.TraceWarning("Could not debug process {0} because another debugger is already attached to it", pid);
                return;
            }

            CommandFile = Environment.ExpandEnvironmentVariables(String.Format(@"%temp%\WindbCommand_{0}.txt", Pid));
            LogFileName = Environment.ExpandEnvironmentVariables(String.Format(@"%temp%\WindbLog_{0}.txt", Pid));


            // Start Debug Server and wait until it is accepting commands.
            // We check this by executing a script on startup which does clear the console
            string startupSyncScript = GenerateStartupScript(toIgnore); 
       

            StartPolicy.FileName = CdpPath;
            StartPolicy.Arguments = String.Format("-server npipe:pipe=process_{0} -p {0} -cf {1}", pid, startupSyncScript);
            StartPolicy.WindowStyle = ForceDebuggerWindowVisible ? ProcessWindowStyle.Normal : debuggerWindowStyle;
            CdbServer = Process.Start(StartPolicy);

            WaitUntilDebugServerIsUp();
        }

        private string GenerateStartupScript(DebuggerEvents toIgnore)
        {
            List<string> commands = new List<string>
            {
                String.Format(".logopen {0}", LogFileName),
                ".childdbg 0",
            };

            foreach(DebuggerEvents debuggerEv in Enum.GetValues(typeof(DebuggerEvents)))
            {
                if( debuggerEv != DebuggerEvents.None && debuggerEv != DebuggerEvents.Default )
                {
                    if( (toIgnore & debuggerEv) == debuggerEv)
                    {
                        // -c \".symfix;.reload;kv;.loadby sos clr;!ClrStack;!pe\"
                        commands.Add(String.Format("sxn  {0}", DebuggerEventMapper.Mapper[debuggerEv])); 
                    }
                }
            }

            commands.AddRange( new string[] 
            {
               ".cls",
                ".logclose",
                "g",
            });

            return GenerateScript(commands.ToArray(), false);
        }

        private void WaitUntilDebugServerIsUp()
        {
            while (true && !CdbServer.HasExited)
            {
                try
                {
                    string output = File.ReadAllText(LogFileName);
                    if (output.Contains(".cls"))
                    {
                        break;
                    }
                    Thread.Sleep(1000); // wait until all output is written.
                }
                catch (IOException)
                {
                    // could happen while the file is still open
                    Thread.Sleep(1000);
                    continue;
                }
                catch (UnauthorizedAccessException)
                {
                    // could happen while the file is still open
                    Thread.Sleep(1000);
                    continue;
                }
            }

            if (CdbServer.HasExited)
            {
                throw new NotSupportedException(String.Format("Windbg server process could not be started. Exit Code: {0:X}", CdbServer.ExitCode));
            }
        }

        /// <summary>
        /// Start process and attach debugger to it after it has started
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="args"></param>
        public CdbEng(string fileName, string args = "")
            : this(Process.Start(fileName, args).Id)
        {
        }

        /// <summary>
        /// Execute a debugger command and do return the output from the debugger.
        /// </summary>
        /// <param name="cmd">Debugger command to execute</param>
        /// <param name="noScriptWrapper">if false a .remote_exit g is appended to resume the remote debugger instance again. If true only the supplied command is executed.</param>
        /// <returns>Output returned by Windbg.</returns>
        public string[] Execute(string cmd, bool noScriptWrapper=false)
        {
            return Execute(new string[] { cmd }, noScriptWrapper);
        }

        /// <summary>
        /// Execute an array of debugger command and do return the output from the debugger.
        /// </summary>
        /// <param name="cmds">Debugger commands to execute</param>
        /// <param name="noScriptWrapper">if false a .remote_exit g is appended to resume the remote debugger instance again. If true only the supplied command is executed.</param>
        /// <returns>Output returned by Windbg.</returns>
        public string[] Execute(string[] cmds, bool noScriptWrapper = false)
        {
            if (!CanDebugProcess)
            {
                return new string[0];
            }

            var windbgScript = GenerateScript(cmds, noScriptWrapper);
            File.Delete(LogFileName);
            // connect to debugging session server process, do execute commands
            // and pipe output to a file to enable 
            // self debugging. You cannot read from a pipe while a debugger connects to you and suspends the process while it is executing debugger commands.
            StartPolicy.FileName = "cmd.exe";
            StartPolicy.Arguments = String.Format("/C \"\"{0}\" -remote npipe:server=127.0.0.1,pipe=process_{1} -bonc -cf \"{2}\" > \"{3}\"\"", 
                                                            CdpPath,
                                                            Pid, 
                                                            windbgScript,
                                                            LogFileName);
            var cmd = Process.Start(StartPolicy);
            cmd.WaitForExit();
            string[] output = ReadOutput(cmds);
            return output;
        }


        /// <summary>
        /// Read Output which does return all lines after the last executed command from the debugger.
        /// </summary>
        /// <param name="cmds">executed commands</param>
        /// <returns>output after the last executed command</returns>
        private string[] ReadOutput(string[] cmds)
        {
            var outputs = File.ReadAllLines(LogFileName);
            int start = outputs.Length - 1;
            foreach (var current in cmds)
            {
                string cmdLine = CmdPrefix + current;
                for (; start >= 0; start--) // scan backwards from output until the output of all commands is found
                {
                    if (outputs[start].IndexOf(cmdLine) != -1)
                        break;
                }
            }

            return outputs.Skip(start).ToArray();
        }

        const string CmdPrefix = "> ";

        /// <summary>
        /// Generate a windbg script file and return the path to it.
        /// </summary>
        /// <param name="cmds">Windbg script commands to execute</param>
        /// <param name="noScriptWrapper">Do not generate the default wrapper to detach from a remote debugger instance</param>
        /// <returns>Path to generated Windbg script file.</returns>
        private string GenerateScript(string[] cmds, bool noScriptWrapper)
        {
            string fullScript = null;
            if (noScriptWrapper == true)
            {
                fullScript = String.Join(Environment.NewLine, cmds);
            }
            else
            {
                fullScript = CommandScript
                                     .Replace(CommandToken, String.Join(Environment.NewLine, cmds));
            }

            Debug.Print("Generate script at {0} with content {1}{2}", CommandFile, Environment.NewLine, fullScript);
            File.WriteAllText(CommandFile, fullScript);
            return CommandFile;
        }

        #region IDisposable Members
        /// <summary>
        /// Close the debugger
        /// </summary>
        public void Dispose()
        {
            if (!IsDisposed)
            {
                Dispose(true);
                IsDisposed = true;
            }
        }

        /// <summary>
        /// Close the debugger
        /// </summary>
        /// <param name="bDisposing">always true</param>
        protected virtual void Dispose(bool bDisposing)
        {
            Debug.Print("CDBEng.Dispose CanDebugProcess {0}, CdbServer.HasExited {1}",CanDebugProcess, CdbServer.HasExited);

            if (CanDebugProcess && CdbServer != null && !CdbServer.HasExited)
            {
                Debug.Print("Wait for debug server exit");
                Execute("qqd");  // Detach and quit windbg server process
                CdbServer.WaitForExit();

                File.Delete(CommandFile); // clean up temp files
                File.Delete(LogFileName);
            }
        }
        #endregion
    }
}
